package br.ufpe.cin.imlm2.jbeat.junite;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import br.ufpe.cin.imlm2.jbeat.model.ILightWeightType;
import br.ufpe.cin.imlm2.jbeat.model.LightWeightClass;
import br.ufpe.cin.imlm2.jbeat.model.LightWeightMethod;
import br.ufpe.cin.imlm2.jbeat.model.LightWeightScene;
import br.ufpe.cin.imlm2.jbeat.model.LightWeightVectorType;
import br.ufpe.cin.imlm2.jbeat.model.factory.DumpReadException;
import br.ufpe.cin.imlm2.jbeat.model.factory.DumpReader;
import br.ufpe.cin.imlm2.jbeat.paths.ExceptionPath;
import br.ufpe.cin.imlm2.jbeat.paths.SimpleExceptionPath;
import br.ufpe.cin.imlm2.jbeat.paths.build.ForwardExceptionPathBuilder;
import br.ufpe.cin.imlm2.jbeat.paths.report.PathMetrics;
import br.ufpe.cin.imlm2.util.Naming;
import br.ufpe.cin.imlm2.util.options.Option;
import br.ufpe.cin.imlm2.util.options.OptionsParser;
import br.ufpe.cin.imlm2.util.options.ParsingException;
import static br.ufpe.cin.imlm2.util.Naming.getShortName;

public class JUniteTestCaseGenerator {

	private static final GeneratorOptions parser = new GeneratorOptions();

	/**
	 * @param args
	 * @throws ParsingException 
	 * @throws IOException 
	 * @throws FileNotFoundException 
	 * @throws DumpReadException 
	 * @throws TestCaseWriterException 
	 */
	public static void main(String[] args) throws ParsingException, FileNotFoundException, IOException, DumpReadException {

		List<String> classDump = parser.parse(args);
		if(classDump.size() > 0) {
			String dump = classDump.get( 0 );
			DumpReader reader = new DumpReader( new FileInputStream( dump ) );
			LightWeightScene scene = reader.readDump();

			LightWeightClass exception = scene.getLightWeightClass(classDump.get(1));
			List<LightWeightMethod> methods;
			if(parser.getEndPoints().size() > 0){
				for (String methodSubSignature : parser.getEndPoints()) {
					String className = Naming.getPackage(methodSubSignature);
					String methName = Naming.getShortName(methodSubSignature);
					if(className == null || methName == null)
						throw new IllegalArgumentException("Invalid method subsignature " +
								methodSubSignature);
					LightWeightClass lightWeightClass = 
							scene.getLightWeightClass(className);
					if(lightWeightClass == null)
						throw new IllegalArgumentException("Unknown class " + className);
					methods = lightWeightClass.getMethods(methName);
					if(parser.isEscaping()){

					} else{
						generateTestForTrapped(exception, methods);									
					}
				}
			} else{
				methods = new ArrayList<>();
				methods.add(scene.getMain());
				if(parser.isEscaping()){

				} else{
					generateTestForTrapped(exception, methods);									
				}
			}

		} else {
			System.out.println("Invalid format.");
			System.out.println(parser.getHelp());
		}

	}

	private static void generateTestForTrapped(LightWeightClass exception,
			List<LightWeightMethod> methods) throws IOException {
		try {
			for (int j = 0; j < methods.size(); j++) {
				LightWeightMethod meth = methods.get(j);
				if(meth.getTrappedMap().containsKey(exception)) {
					JUnitETestCaseWriter writer = new JUnitETestCaseWriter( 
							getTestCaseClassName(meth) + "_" + j, parser.getPath(), null );
					ForwardExceptionPathBuilder builder;
					if(parser.getMaxLength() >= 0) {
						builder = new ForwardExceptionPathBuilder( exception, meth,
								parser.getMaxLength(), null,false );
					} else {
						builder = new ForwardExceptionPathBuilder( exception, meth,	null);
					}
					Collection<ExceptionPath> trappedPaths = builder.buildTrappedExceptionPaths();
					if(parser.isSample()){
						int numberOfStubs = 
								Math.max(5,(int)(0.10 * trappedPaths.size())); // 10% of the paths
						List<ExceptionPath> allPaths = new ArrayList<>(trappedPaths);
						Collection<ExceptionPath> paths = new HashSet<>();
						List<ExceptionPath> longestPaths = 
								PathMetrics.longestPaths(trappedPaths);
						List<ExceptionPath> shortestPaths =
								PathMetrics.shortestPaths(trappedPaths);
						List<ExceptionPath> averagePaths = 
								PathMetrics.averagePaths(trappedPaths);
						
						if(numberOfStubs-- > 0){
							tryAdd(paths, longestPaths);
						}
						
						if(numberOfStubs-- > 0){
							tryAdd(paths, shortestPaths);
						}
						
						if(numberOfStubs-- > 0){
							tryAdd(paths, averagePaths);
						}
						
						int failedAttempts = 0, attempts = 0;
						while(numberOfStubs > 0 && failedAttempts < 5){
							boolean added = false;
							if(attempts % 4 == 0){ // Get one of the longest
								added = tryAdd(paths, longestPaths);
							} else if(attempts % 3 == 0){ // Get an average on
								added = tryAdd(paths, averagePaths);
							} else if(attempts % 2 == 0){ // Get one of the shortest
								added = tryAdd(paths, shortestPaths);
							} else { // Get a random one
								added = tryAdd(paths, allPaths);
							}
							
							if(added){
								numberOfStubs--;
								failedAttempts = 0;
							} else{
								failedAttempts++;
							}
							attempts++;
						}
						
						Iterator<ExceptionPath> it = paths.iterator();
						if(it.hasNext()){
							ExceptionPath next = it.next();
							for ( int i = 0; it.hasNext(); i++ , next = it.next()) {
								generatedMethodStub(meth, writer,
										exception, next, i);
							}
						}		
					} else {
						Collection<SimpleExceptionPath> paths = builder.getTrappedPaths();
						Iterator<SimpleExceptionPath> it = paths.iterator();
						if(it.hasNext()){
							ExceptionPath next = it.next();
							for ( int i = 0; it.hasNext(); i++ , next = it.next()) {
								generatedMethodStub(meth, writer,
										exception, next, i);
							}
						}						
					}
					writer.write();
				}
			}
		} catch (TestCaseWriterException e) {

			e.printStackTrace();
		}
	}

	private static boolean tryAdd(Collection<ExceptionPath> paths,
			List<ExceptionPath> pathsToAdd) {
		boolean added = false;
		while(pathsToAdd.size() > 0 && 
				!(added = paths.add(pathsToAdd.remove(0))));
		return added;
	}

	private static void generatedMethodStub(
			LightWeightMethod lightWeightMethod, JUnitETestCaseWriter writer,
			LightWeightClass exception, ExceptionPath next, int j)
					throws TestCaseWriterException {
		List<String> path = new ArrayList<>();
		for ( LightWeightMethod meth : next ) {
			String name = meth.getDeclaringClass().getFullyQualifiedName();
			name += "." + meth.getName();
			path.add(  name );
		}

		writer.addTestMethodStub( getTestCaseMethodName(lightWeightMethod,
				exception,j), exception.getFullyQualifiedName(), path );
	}

	private static String getTestCaseMethodName( LightWeightMethod m,
			LightWeightClass exception, int j ) {
		LightWeightClass declaringClass = m.getDeclaringClass();
		String paramsString = "_";
		String shortName = generateMethodSignature(m);
		shortName = shortName.replace("<", "").replace(">", "").
				replace("init", getShortName(declaringClass.getFullyQualifiedName())).
				replace("clinit", "static");
		return "test_" + shortName + paramsString +  
				getShortName( exception.getFullyQualifiedName()) + "_"+ j;
	}


	private static String getTestCaseClassName(LightWeightMethod m) {
		LightWeightClass declaringClass = m.getDeclaringClass();
		return declaringClass.getFullyQualifiedName() + "_" + 
				getShortName( m.getName() ).replace("<init>", 
				getShortName( declaringClass.getFullyQualifiedName()));
	}

	private static class GeneratorOptions extends OptionsParser {

		public static final String PATH = "path";
		public static final String ENDPOINTS = "endpoints";
		public static final String ESCAPING = "escaping";
		public static final String MAX_LENGTH ="maxlength";
		public static final String SAMPLE = "sample";
		
		public GeneratorOptions() {
			super("JUnite TestCase Generator");
			this.addOption( new Option( PATH, null, "Output path", System.getProperty("user.dir")) );
			this.addOption(new Option(ENDPOINTS, null, "End points to start generating stubs for." +
					"If no end point is given 'main' will be assumed.",
					new ArrayList<String>(), String.class));
			this.addOption(new Option(ESCAPING, null, "Generate stubs for exception escaping the " +
					"given end points", Boolean.FALSE));
			this.addOption(new Option(MAX_LENGTH, null, "Max path length for the traces in" +
					" the method stubs", 5));
			this.addOption(new Option(SAMPLE, null, "Use a \"random\" sampling to chose the paths",
					Boolean.FALSE));
			this.setUsage("USAGE:\n\t java -cp JBEAT_JAR_PATH " + JUniteTestCaseGenerator.class.getName() + " options*");
		}

		public String getPath(){
			return (String) this.getOptionInternal(PATH).getValue();
		}

		@SuppressWarnings("unchecked")
		public List<String> getEndPoints(){
			return (List<String>) this.getOptionInternal(ENDPOINTS).getValue();
		}

		public boolean isEscaping(){
			return (Boolean)this.getOptionInternal(ESCAPING).getValue();
		}
		
		public int getMaxLength(){
			return (Integer) this.getOptionInternal(MAX_LENGTH).getValue();
		}
		
		public boolean isSample(){
			return (Boolean) this.getOptionInternal(SAMPLE).getValue();
		}
	}

	private static String generateMethodSignature(LightWeightMethod m){
		String sig = getShortName(m.getName());
		List<ILightWeightType> parameters = m.getParameters();
		if(parameters.size() > 0) {
			sig += "_$_";
			List<String> typeStrings = typesToStrings(parameters);
			for (String str : typeStrings) {
				sig += str + "__";
			}
			sig = sig.substring(0, sig.length() - 1);
			sig += "$";
		}
		return sig;
	}

	private static List<String> typesToStrings(List<ILightWeightType> typs){
		List<String> strings = new ArrayList<>();
		for (ILightWeightType iLightWeightType : typs) {
			String typeString;
			typeString = typeToString(iLightWeightType);
			strings.add(typeString);
		}
		return strings;
	}

	private static String typeToString(ILightWeightType iLightWeightType) {
		String typeString;
		if(iLightWeightType instanceof LightWeightVectorType){
			LightWeightVectorType vecType = (LightWeightVectorType) iLightWeightType;
			int numberOfDimensions = vecType.getNumberOfDimensions();
			String componentFullName = vecType.getComponentType().getFullyQualifiedName();
			typeString = getShortName(componentFullName) + "$" + numberOfDimensions;
		} else {
			typeString = getShortName(iLightWeightType.getFullyQualifiedName());
		}
		return typeString;
	}

}
